package org.codefilarete.tool.collection;

import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Supplier;

/**
 * @author Guillaume Mary
 */
public final class Maps {
	
	/**
	 * Allows chaining of the {@link ChainingMap#add(Object, Object)} method and then easily create a Map
	 * @return a new Map instance
	 */
	public static <K, V> ChainingMap<K, V> asMap(K key, V value) {
		return new ChainingMap<K, V>().add(key, value);
	}
	
	/**
	 * Allows chaining of the {@link ChainingHashMap#add(Object, Object)} method and then easily create a Map
	 * @return a new HashMap instance
	 */
	public static <K, V> ChainingHashMap<K, V> asHashMap(K key, V value) {
		return new ChainingHashMap<K, V>().add(key, value);
	}
	
	/**
	 * A simple factory method that takes only expected key and value types, made to avoid casting when using method factory with key and value
	 * (that next call to add(..) requires casting of key and/or values) 
	 * 
	 * @param keyType key type
	 * @param valueType value type
	 * @param <K> key type
	 * @param <V> value type
	 * @return a new {@link HashMap} that allows chaining of additional key and value
	 */
	public static <K, V> ChainingHashMap<K, V> forHashMap(Class<K> keyType, Class<V> valueType) {
		return new ChainingHashMap<>();
	}
	
	/**
	 * Returns a {@link Map} of given {@link Map}s values that are mapped on the same keys in both {@link Map}s.
	 * 
	 * @param map1 a {@link Map}
	 * @param map2 a {@link Map}
	 * @param <K> {@link Map}s key type
	 * @param <V1> first {@link Map} values type
	 * @param <V2> second {@link Map} values type
	 * @return a {@link Map} of values that are in both {@link Map}s and mapped under the same keys
	 */
	public static <K, V1, V2> Map<V1, V2> innerJoin(Map<K, V1> map1, Map<K, V2> map2) {
		Map<V1, V2> result = new HashMap<>();
		map1.forEach((k, v) -> {
			if (map2.containsKey(k)) {
				result.put(v, map2.get(k));
			}
		});
		return result;
	}
	
	/**
	 * Returns a {@link Map} of given first {@link Map} keys and second {@link Map} values
	 * by joining them on first {@link Map} values and second {@link Map} keys.
	 *
	 * @param map1 a {@link Map}
	 * @param map2 a {@link Map}
	 * @param <K> {@link Map}s key type
	 * @param <V1> first {@link Map} values and second {@link Map} keys type
	 * @param <V2> second {@link Map} values type
	 * @return a {@link Map} of first {@link Map} keys and second {@link Map} values joined on first {@link Map} values and second {@link Map} keys.
	 */
	public static <K, V1, V2> Map<K, V2> innerJoinOnValuesAndKeys(Map<K, V1> map1, Map<V1, V2> map2) {
		return innerJoinOnValuesAndKeys(map1, map2, HashMap::new);
	}
	
	/**
	 * Returns a {@link Map} of given first {@link Map} keys and second {@link Map} values
	 * by joining them on first {@link Map} values and second {@link Map} keys.
	 *
	 * @param map1 a {@link Map}
	 * @param map2 a {@link Map}
	 * @param supplier supplier for resulting {@link Map}, allows changing its type, size it, etc.
	 * @param <K> {@link Map}s key type
	 * @param <V1> first {@link Map} values and second {@link Map} keys type
	 * @param <V2> second {@link Map} values type
	 * @return a {@link Map} of first {@link Map} keys and second {@link Map} values joined on first {@link Map} values and second {@link Map} keys.
	 */
	public static <K, V1, V2, M extends Map<K, V2>> M innerJoinOnValuesAndKeys(Map<K, V1> map1, Map<V1, V2> map2, Supplier<M> supplier) {
		M result = supplier.get();
		map1.forEach((k, v1) -> {
			V2 v2 = map2.get(v1);
			result.put(k, v2);
		});
		return result;
	}
	
	/**
	 * Puts all elements of map1 and map2 into a new {@link HashMap}.
	 * Made to have a putAll(..) method inlined.
	 * 
	 * @param map1 any non null {@link Map}
	 * @param map2 any non null {@link Map}
	 * @param <K> {@link Map}s key type
	 * @param <V> {@link Map}s value type
	 * @return a new {@link HashMap} containing elements of map1 and
	 */
	public static <K, V> Map<K, V> putAll(Map<? extends K, ? extends V> map1, Map<? extends K, ? extends V> map2) {
		Map<K, V> result = new HashMap<>(map1);
		result.putAll(map2);
		return result;
	}
	
	/**
	 * Simple {@link LinkedHashMap} that allows to chain calls to {@link #add(Object, Object)} (same as put) and so quickly create a Map.
	 * 
	 * @param <K>
	 * @param <V>
	 */
	public static class ChainingMap<K, V> extends LinkedHashMap<K, V> {
		
		public ChainingMap() {
			super();
		}
		
		public ChainingMap<K, V> add(K key, V value) {
			put(key, value);
			return this;
		}
	}
	
	/**
	 * Simple {@link HashMap} that allows to chain calls to {@link #add(Object, Object)} (same as put) and so quickly create a Map.
	 *
	 * @param <K>
	 * @param <V>
	 */
	public static class ChainingHashMap<K, V> extends HashMap<K, V> {
		
		public ChainingHashMap() {
			super();
		}
		
		public ChainingHashMap<K, V> add(K key, V value) {
			put(key, value);
			return this;
		}
	}
	
	
	/**
	 * Simple {@link TreeMap} that allows to chain calls to {@link #add(Object, Object)} (same as put) and so quickly create a Map.
	 *
	 * @param <K>
	 * @param <V>
	 */
	public static class ChainingComparingMap<K, V> extends TreeMap<K, V> {
		
		public ChainingComparingMap(Comparator<K> comparator) {
			super(comparator);
		}
		
		public ChainingComparingMap<K, V> add(K key, V value) {
			put(key, value);
			return this;
		}
	}
}
